﻿using System;
using System.Collections;
using System.Collections.Generic;
using SharpDX;
using SharpDX.D3DCompiler;
using SharpDX.Diagnostics;
using SharpDX.Direct3D;
using SharpDX.Direct3D10;
using SharpDX.DXGI;
using SharpDX.Windows;

using DX11Engine;
using DX11Engine.Diagnostics;

using Buffer = SharpDX.Direct3D10.Buffer;
using Device = SharpDX.Direct3D10.Device;
using DriverType = SharpDX.Direct3D10.DriverType;
using System.Diagnostics;

namespace DX11Engine
{
    

    public class JDXRender
    {
        // Properties for device:
        private Device _device;
        public Device Device { get { return _device; } }
        public RenderForm Form { get; set; }
        public Texture2D BackBuffer { get; set; }

        // Diagnostics:
        public int WorldRenderTime { get; private set; }
        public int AvgEntityRenderTime { get; private set; }

        // Entities:       
        public List<J3DObject> Entities { get; private set; }
        public List<CameraViewport> CameraViewports { get; private set; }
        public JEntityFactory EntityFactory { get; private set; }

        public Vector3 LastPickedDirection { get; private set; }

        // Internal variables for device:
        private SwapChainDescription _desc;
        private SwapChain swapChain;
        private Factory _factory;
        private RenderTargetView _renderView;
        private DepthStencilView _depthView;
        private Buffer _contantBuffer;
        private Matrix WorldViewMatrix, WorldMatrix;

        // Internal performance counters:
        private RenderingDiagnostic rndrDiag = new RenderingDiagnostic();
        private int entsRendered = 0;

        /// <summary>Renders the current 3D Scene</summary>
        /// <param name="cm">Camera Viewport to render</param>
        /// <param name="pickX">X position to perform pick</param>
        /// <param name="pickY">Y position to perform pick</param>
        public void JRender(CameraViewport cm, int pickX = -1, int pickY = -1)
        {
            rndrDiag.BeginSpecific("FrameTime");

            rndrDiag.BeginSingle();
            

            // Clear render view and depth buffer:
            this.ClearColor = new Color4(0f, .0117f, .435f, .839f);
            _device.ClearDepthStencilView(_depthView, DepthStencilClearFlags.Depth, 1.0f, 0);
            _device.ClearRenderTargetView(_renderView, this.ClearColor);

            // Setup Output:
            _device.Rasterizer.SetViewports(cm.Viewport);
            _device.OutputMerger.SetTargets(_depthView, _renderView);
            _device.VertexShader.SetConstantBuffer(0, _contantBuffer);

            // Prepare matrices
            Matrix viewProj = Matrix.Multiply(cm.ViewMatrix, cm.ProjectionMatrix);

            rndrDiag.EndSingle("FrameSetup");

            rndrDiag.BeginSingle();
            // Handle picking:
            if (pickX > -1 && pickY > -1) { viewProj = CameraPick(cm, pickX, pickY, viewProj); }
            rndrDiag.EndSingle("PickingCalculation");

            // Reset counters:
            this.entsRendered = 0;
            for (int x = 0; x < Entities.Count; x++)
            {
                
                if (Entities[x].IsVisible)
                {
                    rndrDiag.BeginSpecific("ProcessingEntity");
                    this.entsRendered++;

                    // Calculate world and view matrixes relative to entity:
                    rndrDiag.BeginSingle();
                    WorldMatrix = Matrix.Scaling(Entities[x].Scale) *
                            Matrix.RotationYawPitchRoll(Entities[x].Rotation.Y, Entities[x].Rotation.X, Entities[x].Rotation.Z) *
                            Matrix.Translation(Entities[x].Position);
                    WorldViewMatrix = WorldMatrix * viewProj;
                    rndrDiag.EndSingle("MatrixMath");

                    // Pass our matrixes and settings to our effects file: (Avg 51 ticks)
                    Entities[x].SurfaceData.EffectHandle.GetVariableByName("worldViewProj").AsMatrix().SetMatrix(WorldViewMatrix);
                    Entities[x].SurfaceData.EffectHandle.GetVariableByName("worldMatrix").AsMatrix().SetMatrix(WorldMatrix);
                    Entities[x].SurfaceData.EffectHandle.GetVariableByName("lightDirection").AsVector().Set(new Vector3(0,0,1f));

                    // Call entity's draw method:
                    rndrDiag.BeginSpecific("RenderingEntity");
                    Entities[x].DrawEntity(_device, rndrDiag);
                    rndrDiag.EndSpecific("RenderingEntity");

                    rndrDiag.EndSpecific("ProcessingEntity");
                }
                
            }
            rndrDiag.AddToStatistic("EntitiesRenderedPerFrame", entsRendered);

            rndrDiag.EndSpecific("FrameTime");

            if (swapChain.ContainingOutput != null)
            {swapChain.ContainingOutput.WaitForVerticalBlank();}

            rndrDiag.BeginSingle();
            swapChain.Present(0, PresentFlags.None);
            rndrDiag.EndSingle("PresentTiming");
        }

        

        private Matrix CameraPick(CameraViewport cm, int pickX, int pickY, Matrix viewProj)
        {
            Vector3 nearPick = Vector3.Unproject(new Vector3(new Vector2(pickX, pickY), 0),
                0, 0, cm.Viewport.Width, cm.Viewport.Height, 1, 5, viewProj);
            Vector3 farPick = Vector3.Unproject(new Vector3(new Vector2(pickX, pickY), 1),
                0, 0, cm.Viewport.Width, cm.Viewport.Height, 1, 5, viewProj);
            Vector3 PickDirection = farPick - nearPick;
            PickDirection.Normalize();

            LastPickedDirection = PickDirection;
            return viewProj;
        }

        public JDXRender(RenderForm form, bool Windowed = true, int Width = 0, int Height = 0)
        {
            this.Form = form;

            this.Entities = new List<J3DObject>();
            this.CameraViewports = new List<CameraViewport>();
            CameraViewports.Add(new CameraViewport(Windowed ? Form.ClientSize.Width : Width, Windowed ? Form.ClientSize.Height : Height));

            Configuration.EnableObjectTracking = true;

            // SwapChain Description:
            _desc = new SwapChainDescription()
            {
                BufferCount = 1,
                ModeDescription =
                    new ModeDescription(Windowed ? Form.ClientSize.Width : Width, Windowed ? Form.ClientSize.Height : Height,
                                        new Rational(60, 1), Format.R8G8B8A8_UNorm),
                IsWindowed = Windowed,
                OutputHandle = Form.Handle,
                SampleDescription = new SampleDescription(1, 0),
                SwapEffect = SwapEffect.Discard,
                Usage = Usage.RenderTargetOutput,

            };

            // Create Device and SwapChain
            Device.CreateWithSwapChain(DriverType.Hardware, DeviceCreationFlags.Debug, _desc, out _device, out swapChain);
            

            // Create our Entity Factory now that we have a device:
            this.EntityFactory = new JEntityFactory(_device);

            // Get the reference to our back buffer:
            BackBuffer = Texture2D.FromSwapChain<Texture2D>(swapChain, 0);

            // Set Form to ignore all windows events:
            _factory = swapChain.GetParent<Factory>();
            _factory.MakeWindowAssociation(Form.Handle, WindowAssociationFlags.IgnoreAll);

            // Create Constant Buffer to hold constants between shaders:
            _contantBuffer = new Buffer(_device, Utilities.SizeOf<Matrix>(), ResourceUsage.Default, BindFlags.ConstantBuffer, CpuAccessFlags.None, ResourceOptionFlags.None);

            // Create Depth Buffer & View:
            var depthBuffer = new Texture2D(_device, new Texture2DDescription()
            {
                Format = Format.D32_Float_S8X24_UInt,
                ArraySize = 1,
                MipLevels = 1,
                Width = Windowed ? Form.ClientSize.Width : Width,
                Height = Windowed ? Form.ClientSize.Height : Height,
                SampleDescription = new SampleDescription(1, 0),
                Usage = ResourceUsage.Default,
                BindFlags = BindFlags.DepthStencil,
                CpuAccessFlags = CpuAccessFlags.None,
                OptionFlags = ResourceOptionFlags.None
            });

            // Create depth stencil view for Z-buffer:
            _depthView = new DepthStencilView(_device, depthBuffer);

            // Set the render view to the back buffer:
            _renderView = new RenderTargetView(_device, BackBuffer);
        }
        
        public void Dispose()
        {
            rndrDiag.WritePerfLog();

            Entities.ForEach((e) => e.Dispose());
            //CameraViewports.ForEach((v) => v.Dispose());

            // Release all resources
            if (BackBuffer != null) { BackBuffer.Dispose(); }
            if (_renderView != null) { _renderView.Dispose(); }
            if (_device != null)
            {
                _device.ClearState();
                _device.Flush();
                _device.Dispose();
            }
            if (_renderView != null) { swapChain.Dispose(); }
            if (_renderView != null) { _factory.Dispose(); }
        }

        public Color4 ClearColor { get; set; }
    }
}
